// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.scene;

import bats.Assets;
import bats.control.physics.BurnControl;
import com.jme3.audio.AudioNode;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.collision.shapes.CylinderCollisionShape;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.effect.influencers.ParticleInfluencer;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.NameGenerator;

/**
 * A world component to manage animated flames.
 * <p>
 * Each flame has four main components: a Node which represents the base, a
 * particle emitter (which includes a particle influencer), a ghost control for
 * detecting intrusions, and a point light source.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Flames {
    // *************************************************************************
    // constants

    /**
     * color of light emitted by flames
     */
    final private static ColorRGBA lightColor =
            new ColorRGBA(1f, 0.5f, 0.2f, 1f);
    /**
     * color of particles when they expire
     */
    final private static ColorRGBA particleEndColor = ColorRGBA.Red;
    /**
     * color of particles when they are created
     */
    final private static ColorRGBA particleStartColor =
            new ColorRGBA(1f, 0.9f, 0.3f, 0.3f);
    /**
     * life seconds value which results in a permanent flame
     */
    final public static float forever = Float.POSITIVE_INFINITY;
    /**
     * maximum lifetime of flame particles (in seconds &gt;0f)
     */
    final public static float particleMaxLife = 1f;
    /**
     * minimum lifetime of flame particles (in seconds &gt;0f)
     */
    final public static float particleMinLife = 0.5f;
    /**
     * percentage of particle initial velocity which is random
     */
    final private static float percentRandom = 40f;
    /**
     * base-to-emitter distance (in meters) for a standard-sized flame
     */
    final private static float standardCenterHeight = 0.2f;
    /**
     * the radius (in meters) of a standard-sized flame
     */
    final private static float standardRadius = 0.35f;
    /**
     * base-to-tip distance (in meters) for a standard-sized flame
     */
    final private static float standardTipHeight = 0.8f;
    /**
     * the maximum number of particles per emitter
     */
    final private static int maxParticleCount = 16;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(Flames.class.getName());
    /**
     * type of mesh for particle emitters
     */
    final private static ParticleMesh.Type meshType =
            ParticleMesh.Type.Triangle;
    /**
     * asset path for the crackling sound effect
     */
    final public static String cracklePath = "Sounds/effects/fire.ogg";
    /**
     * prefix for generating spatial names
     */
    final private static String namePrefix = "flame";
    /**
     * asset path for textures
     */
    final private static String texturePath = "Effects/Explosion/flame.png";
    // *************************************************************************
    // fields
    /**
     * scene in which these flames reside: set by constructor
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate for the specified scene.
     *
     * @param scene where these flames reside (not null)
     */
    Flames(SceneNode scene) {
        super();

        assert scene != null;
        this.scene = scene;
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Create a standard-size permanent animated flame and add it to the scene.
     *
     * @param baseLocation world coordinates for the flame's base (not null)
     */
    void add(Vector3f baseLocation) {
        assert baseLocation != null;

        add(baseLocation, 1f);
    }

    /**
     * Create a permanent animated flame and add it to the scene.
     *
     * @param baseLocation world coordinates for the flame's base (not null)
     * @param relativeSize the flame's size relative to standard (&gt;0)
     */
    void add(Vector3f baseLocation, float relativeSize) {
        assert baseLocation != null;
        assert relativeSize > 0f : relativeSize;

        add(baseLocation, relativeSize, forever);
    }

    /**
     * Create an animated flame and add it to the scene.
     *
     * @param baseLocation world coordinates for the flame's base (not null)
     * @param relativeSize the flame's size relative to standard (&gt;0)
     * @param lifeSeconds seconds until the flame runs out fuel (&gt;0)
     */
    void add(Vector3f baseLocation, float relativeSize,
            float lifeSeconds) {
        assert baseLocation != null;
        assert relativeSize > 0f : relativeSize;
        assert lifeSeconds > 0f : lifeSeconds;
        /*
         * Create a node to represent the base of the flame and add
         * that node to the scene.
         */
        NameGenerator nameGenerator = scene.getNameGenerator();
        String name = nameGenerator.unique(namePrefix);
        Node node = new Node(name);
        scene.getWorld().attach(node);
        MySpatial.setWorldLocation(node, baseLocation);
        RenderQueue.ShadowMode shadowMode = RenderQueue.ShadowMode.Off;
        node.setShadowMode(shadowMode);
        /*
         * Create the particle emitter and attach it to the base.
         */
        String emitterName = name + " emitter";
        ParticleEmitter emitter =
                new ParticleEmitter(emitterName, meshType, maxParticleCount);
        node.attachChild(emitter);
        initializeEmitter(emitter);
        /*
         * Add a light source at the center.
         */
        PointLight light = new PointLight();
        light.setColor(lightColor);
        scene.getLighting().addLight(light, node);
        /*
         * Add a BurnControl to shrink the flame and dim its light as it
         * consumes fuel.
         */
        BurnControl burnControl =
                new BurnControl(relativeSize, lifeSeconds, scene);
        scene.getWorld().getPhysics().addObject(burnControl);
        node.addControl(burnControl);
        /*
         * Load and play the crackle sound effect.
         */
        AudioNode crackleNode = Assets.loadBufferedAudio(cracklePath);
        crackleNode.setLooping(true);
        node.attachChild(crackleNode);
        crackleNode.play();
    }

    /**
     * Generate a collision shape for a flame.
     *
     * @param relativeSize the flame's size relative to standard (&gt;0)
     * @return a new instance
     */
    public CollisionShape createCollisionShape(float relativeSize) {
        assert relativeSize > 0f : relativeSize;

        float tipHeight = standardTipHeight * relativeSize; // meters
        float radius = standardRadius * relativeSize; // meters
        float halfHeight = tipHeight - radius; // meters
        Vector3f halfExtents = new Vector3f(radius, halfHeight, radius);
        float scaleFactor = scene.getScaleFactor();
        halfExtents.divideLocal(scaleFactor); // convert to world units
        CollisionShape shape =
                new CylinderCollisionShape(halfExtents, PhysicsSpace.AXIS_Y);

        Vector3f offset = new Vector3f(0f, halfHeight, 0f);
        offset.divideLocal(scaleFactor);
        CompoundCollisionShape compound = new CompoundCollisionShape();
        compound.addChildShape(shape, offset);

        return compound;
    }

    /**
     * Extinguish a flame by deleting it.
     *
     * @param spatial which flame (not null)
     */
    public void extinguish(Spatial spatial) {
        logger.log(Level.INFO, "delete {0}", spatial.getName());
        scene.deleteSpatial(spatial);
    }

    /**
     * Extinguish all the animated flames in the scene.
     */
    void extinguishAll() {
        SceneGraphVisitor visitor = new SceneGraphVisitor() {
            @Override
            public void visit(Spatial spatial) {
                String name = spatial.getName();
                if (NameGenerator.isFrom(name, namePrefix)) {
                    extinguish(spatial);
                }
            }
        };
        scene.getWorld().traverse(visitor);
    }

    /**
     * Get the height of the flame's center above its base.
     *
     * @param relativeSize the flame's size relative to standard (&gt;0)
     * @return height of the flame's center above its base (in meters, &gt;0)
     */
    public float getCenterHeight(float relativeSize) {
        assert relativeSize > 0f : relativeSize;

        float height = standardCenterHeight * relativeSize;

        assert height > 0f : height;
        return height;
    }

    /**
     * Get the emitter associated with a flame.
     *
     * @param node which flame (not null)
     * @return the pre-existing instance (not null)
     */
    public static ParticleEmitter getEmitter(Node node) {
        assert node != null;

        ParticleEmitter result =
                MySpatial.findChild(node, ParticleEmitter.class);
        assert result != null;
        return result;
    }
    // *************************************************************************
    // private methods

    /**
     * Initialize an emitter that's part of a flame.
     *
     * @param emitter which emitter (not null)
     */
    private void initializeEmitter(ParticleEmitter emitter) {
        /*
         * Create material for particles.
         */
        Material material = Assets.createParticleMaterial(texturePath);
        /*
         * Initalize the emitter.
         */
        emitter.setEnabled(scene.isEnabled());
        emitter.setEndColor(particleEndColor);
        emitter.setHighLife(particleMaxLife); // seconds
        emitter.setImagesX(2);
        emitter.setImagesY(2); // 2x2 texture animation
        emitter.setInWorldSpace(true);
        emitter.setLowLife(particleMinLife); // seconds
        emitter.setMaterial(material);
        emitter.setSelectRandomImage(true);
        emitter.setStartColor(particleStartColor);
        /*
         * Initalize the influencer.
         */
        ParticleInfluencer influencer = emitter.getParticleInfluencer();
        influencer.setVelocityVariation(percentRandom / 100f);
    }
}